/*!
 * JsAnimator JavaScript Library 1.0.0
 *  
 * Copyright (c) 2009 Cody Frederick, http://code.google.com/p/jsanimator/
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *--------------------------------------------------------------------------*/

(function() {
    
    // The JsAnimator Library
    var JsAnimator = {
        //
        // Renders html from a url into a target element.
        //  url: the address of the web resource to render
        //  target: the element that will contain the final html
        //  callback: a function to call when rendering is complete
        //      target: the element that the html was rendered in
        //      success: boolean indicating success or failure
        //      statusCode: http status code that was returned when attempting to retreive html from the url
        renderHtml: function (url, target, callback) {
            Helpers.xhrHtml(url, target, callback);
        },
        //
        // Renders an html form from a url into a target element. 
        // Additionally, overrides the form's submission to be asynchronous by posting fields in a json format.
        //  args: an javascript object that contains several properties (shown in call order)
        //      url: (required) the address of the web resource to render; should contain a single form element
        //      target: (required) the element that will contain the html loaded from the url
        //      onLoadSuccess: (optional) a function that will execute after the html has been succesfully load from the url.
        //          typically this is used to add additional functionality to a form
        //      onLoadError: (optional) a function that will execute if the html could not be loaded from the url
        //          target: the element that would contain the html if a failure didn't occur
        //          statusCode: http status code that was returned when attempting to retreive html from the url
        //      onBeforeSubmit: (optional) a function that will execute right before a submission is triggered
        //      onValidate: (optional) a function that will execute before the form is submitted. it should return a boolean on whether or not the form should be submitted
        //          form: the form element that is a candidate to submit
        //      onAfterSubmit: (optional) a function that will execute after the form has been submitted
        //          success: a boolean that indicates whether or not a submission was successful or not; true = successful
        //          json: the json response from the server
        //      onSubmitError: (optional) a function that will execute if a failure occurred while submitting the form
        //          json: the json response from the server
        //          errors: an array of hashes containing the errors from the server or null if not supplied
        //          errorCode: contains a code returned from the server or null if not supplied
        //          errorMessage: contains a message returned from the server or null if not supplied
        renderForm: function (args) {
            // only the url and target arguments are required
            if (!args || !args.url || !args.target) {
                throw ("Invalid argument list.");
            } else {
                // convert the target argument to a dom element; if needed
                if (typeof args.target === "string") {
                    args.target = document.getElementById(args.target);
                }
            }
            // load the html containing the form to infuse
            Helpers.xhrHtml(args.url, args.target, function (target, success, statusCode) {
                if (success) {
                    // execute code after the load was successful
                    if (args.onLoadSuccess) { 
                        args.onLoadSuccess(); 
                    }
                    // grab the first form that was loaded
                    var form = args.target.getElementsByTagName("form")[0];
                    // wire up the onSubmit function
                    form.onsubmit = function () {
                        // execute code before anything is done inside onsubmit
                        if (args.onBeforeSubmit) {
                            args.onBeforeSubmit();
                        }
                        var isValid = false;
                        // execute code to validate the form
                        if (args.onValidate) { 
                            isValid = args.onValidate(form);
                        } else {
                            isValid = true;
                        }
                        // check to see if the form is valid
                        if (isValid) {
                            // make the json form post
                            Helpers.jsonFormPost(form, function(success, json, errors, errorCode, errorMessage) {
                                if (!success) {
                                    // the post was unsuccessful; loop through any errors returned by the server
                                    // example json:
                                    //  errors : {"field2": "message", "field2": "message"}
                                    for (var key in errors) {
                                        // attempt to get the field element that has an error
                                        var formElement = form.elements[key];
                                        // if found, then add the "has-error" class name
                                        if (formElement) {
                                            Helpers.addClassName(formElement, "has-error");
                                            // error message is stored in: errors[e][key]
                                        }
                                    }
                                }
                                // execute code after the submit was completed
                                if (args.onAfterSubmit) {
                                    args.onAfterSubmit(success, json);
                                }
                                // make sure to raise onSubmitError after onAfterSubmit
                                // this is to allow users to redirect, etc first
                                if (!success) {
                                    // execute code after a form submit failed
                                    if (args.onSubmitError) {
                                        args.onSubmitError(json, errors, errorCode, errorMessage);
                                    }
                                }
                            });
                        }
                        return false;
                    };
                } else {
                    // execute code if the html failed to load
                    if (args.onLoadError) { 
                        args.onLoadError(target, statusCode); 
                    }
                }
            });
        }
            
    };
    
    // Functions that will extend the functionality of window
    var Extensions = {
        //
        // Retreives parameters from the url
        // The parameters returned will be an associative array; e.g. - "?key=value" => params["key"] = value
        getParams: function() {
            var queryString = location.search.replace("?", "");
            var keyValuePairs = queryString.split("&");
            var params = {};
            for(i in keyValuePairs) {
                var keyValue = keyValuePairs[i].split("=");
                params[keyValue[0]] = keyValue[1];
            }
            return params;
        }
    };
    
    // Functions that are only used by JsAnimator; not made publicly available
    var Helpers = {
        // 
        // Returns a cross-browser instance of XMLHttpRequest
        getXhr: function () {
            return window.ActiveXObject ? new window.ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
        },
        // 
        // Asynchronous JavaScript and JSON
        //  url: the address of the web resource to request
        //  method: the http method to use, only GET and POST are accepted
        //  params: parameters to send during the request
        //  callback: a function to call when request is complete
        xhrJson: function (url, method, params, callback) {
            if (method !== "GET" && method !== "POST") {
                throw ("Invalid HTTP method. Only GET and POST are currently accepted.");
            }
            var xhr = this.getXhr();
            if (method === "GET") {
                url += "?" + encodeURIComponent(params);
                params = null;
            } 
            xhr.open(method, url, true);
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            if (method === "POST") {
                if (params === undefined || params === null) {
                    params = "";
                }
                xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                xhr.setRequestHeader("Content-length", params.length);
                xhr.setRequestHeader("Connection", "close");
            }
            xhr.onreadystatechange = function () {
                var json;
                if (xhr.readyState === 4) {
                    if (xhr.status === 200 || xhr.status === 304) {
                        json = eval("(" + xhr.responseText + ")");
                    }
                    if (callback) { 
                        callback(json); 
                    }
                }
            };
            xhr.send(params);
        },
        // 
        // Asynchronous HTML and HTTP
        //  url: the address of the web resource to get
        //  target: the id of the element that will contain the final html
        //  callback: a function to call when the html is finished loading
        xhrHtml: function (url, target, callback) {
            if (typeof target === "string") {
                target = document.getElementById(target);
            }
            var xhr = this.getXhr();
            xhr.onreadystatechange = function () {
                var success = false;
                if (xhr.readyState === 4) {
                    if (xhr.status === 200 || xhr.status === 304) {
                        target.innerHTML = xhr.responseText;
                        success = true;
                    } else {
                        success = false;
                    }
                    if (callback) {
                        callback(target, success, xhr.status); 
                    }
                }
            };
            xhr.open("GET", url, true);
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xhr.send(null);
        },
        //
        // Enumerates over every input field in a form
        //  form: the form who's element to enumerate through
        //  callback: a function to execute for every field
        eachFormField: function (form, callback) {
            if (typeof(form) === "string") {
                form = document.getElementById(form);
            }
            for (var i = 0; i < form.elements.length; i++) {
                callback(form.elements[i]);
            }
        },
        //
        // Enumerates over every input field in a form and builds a query string
        //  form: the form element to enumerate through
        buildParametersFromForm: function (form) {
            var params = "";
            this.eachFormField(form, function (field) {
                if (params.length > 0) {
                    params += "&";
                }
                params += field.name + "=" + encodeURIComponent(field.value);
            });
            return params;
        },
        //
        // Submits a form in an asynchronous fashion using json
        //  form: the form to submit asynchronously
        //  callback: a function to call when the submission is complete
        //      success: a boolean that indicates whether or not a submission was successful or not; true = successful
        //      json: the json response form the server
        //      errors: an array of hashes containing the errors from the server or null if not supplied
        //      errorCode: contains a code returned from the server or null if not supplied
        //      errorMessage: contains a message returned from the server or null if not supplied
        jsonFormPost: function (form, callback) {
            if (typeof(form) === "string") {
                form = document.getElementById(form);
            }
            this.xhrJson(form.action, form.method.toUpperCase(), this.buildParametersFromForm(form), function (json) {
                if (json && json.status === "success") {
                    callback(true, json);
                } else {
                    var errors = json && json.errors ? eval("(" + json.errors + ")") : null;
                    var errorCode = json && json.errorCode ? json.errorCode : null;
                    var errorMessage = json && json.errorMessage ? json.errorMessage : null;
                    callback(false, json, errors, errorCode, errorMessage);
                }
            });
        },
        //
        //  Adds a class name to an element
        //  element: the element to add a class name. can be a string or an element object
        //  cn: the name of the class to add
        addClassName: function (element, cn) {
            var expr = new RegExp("\\s*" + cn + "\\s*");
            if (typeof(element) === "string") {
                element = document.getElementById(element);
            }
            element.className = element.className.replace(expr, '') + ' ' + cn;
        },
        //
        //  Removes a class name from an element
        //  element: the element to remove a class name. can be a string or an element object
        //  cn: the name of the class to remove
        removeClassName: function (element, cn) {
            var expr = new RegExp("\\s*" + cn + "\\s*");
            if (typeof(element) === "string") {
                element = document.getElementById(element);
            }
            element.className = element.className.replace(expr, '');
        }
    };
  
    // expose the JsAnimator
    window.JsAnimator = JsAnimator;
    
    // parse any query parameters and make available from the params object
    window.params = Extensions.getParams();

})();
